import 'dart:io';
import 'dart:math';

import './run_process.dart';
import 'package:logging/logging.dart';
import 'package:native_assets_cli/native_assets_cli.dart';

// Not ideal:
import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart';

/// Golang artifact builder
class GoBuilder {
  /// Name of the library to build.
  ///
  /// File will be placed in [BuildConfig.outDir].
  final String name;

  /// Asset identifier.
  ///
  /// Used to output the [BuildOutput.assets].
  ///
  /// If omitted, no asset will be added to the build output.
  final String? assetName;

  /// Golang bridge to build.
  ///
  /// Resolved against [BuildConfig.packageRoot].
  ///
  /// Used to output the [BuildOutput.dependencies].
  final String bridgePath;

  /// Sources to build the library.
  ///
  /// Resolved against [BuildConfig.packageRoot].
  ///
  /// Used to output the [BuildOutput.dependencies].
  final List<String> sources;

  /// The dart files involved in building this artifact.
  ///
  /// Resolved against [BuildConfig.packageRoot].
  ///
  /// Used to output the [BuildOutput.dependencies].
  final List<String> dartBuildFiles;

  GoBuilder({
    required this.name,
    required this.assetName,
    required this.bridgePath,
    this.sources = const [],
    this.dartBuildFiles = const [],
  });

  /// Runs the Golang Compiler.
  ///
  /// Completes with an error if the build fails.
  Future<void> run({
    required BuildConfig buildConfig,
    required BuildOutput buildOutput,
    required Logger? logger,
  }) async {
    final outDir = buildConfig.outputDirectory;
    final packageRoot = buildConfig.packageRoot;
    await Directory.fromUri(outDir).create(recursive: true);
    var linkMode = _linkMode(buildConfig.linkModePreference);
    final libUri = outDir.resolve(
      buildConfig.targetOS.libraryFileName(name, linkMode),
    );
    final bridgePath = packageRoot.resolveUri(Uri.file(this.bridgePath));
    final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger);

    if (!buildConfig.dryRun) {
      final compiler = await resolver.resolveCompiler();
      var buildMode = linkMode == StaticLinking() ? "c-archive" : "c-shared";
      var goLib = libUri;

      final env = {
        "CGO_ENABLED": "1",
      };

      String buildArch;
      switch (buildConfig.targetArchitecture) {
        case Architecture.arm:
          buildArch = "arm";
        case Architecture.arm64:
          buildArch = "arm64";
        case Architecture.ia32:
          buildArch = "386";
        case Architecture.x64:
          buildArch = "amd64";
        default:
          throw Exception("Unknown architecture");
      }

      String buildOs;
      switch (buildConfig.targetOS) {
        case OS.windows:
          buildOs = "windows";

        case OS.linux:
          buildOs = "linux";

        case OS.android:
          buildOs = "android";

          // The Android Gradle plugin does not honor API level 19 and 20 when
          // invoking clang. Mimic that behavior here.
          // See https://github.com/dart-lang/native/issues/171.
          final minimumApi =
              buildConfig.targetArchitecture == Architecture.riscv64 ? 35 : 21;
          final targetAndroidNdkApi =
              max(buildConfig.targetAndroidNdkApi!, minimumApi);

          final cc = compiler.uri.resolve(
              './${androidNdkClangTargetFlags[buildConfig.targetArchitecture]!}$targetAndroidNdkApi-clang');
          env["CC"] = cc.toFilePath();

        case OS.iOS:
          buildOs = "ios";
          buildMode = "c-archive";
          goLib = outDir.resolve('out.o');

        case OS.macOS:
          buildOs = "darwin";

        default:
          throw Exception("Unknown os");
      }

      env["GOOS"] = buildOs;
      env["GOARCH"] = buildArch;

      await runProcess(
        executable: Uri.file("go"),
        environment: env,
        arguments: [
          "build",
          "-buildmode=$buildMode",
          if (buildConfig.buildMode != BuildMode.debug) '-ldflags=-s -w',
          '-o',
          goLib.toFilePath(),
          bridgePath.toFilePath(),
        ],
        logger: logger,
        captureOutput: false,
        throwOnUnexpectedExitCode: true,
      );

      if (buildConfig.targetOS == OS.iOS) {
        //xcrun -sdk iphoneos clang -arch armv7 -fpic -shared -Wl,-all_load
        // libmystatic.a -framework Corefoundation -o libmydynamic.dylib
        await runProcess(
          executable: compiler.uri,
          arguments: [
            '-fpic',
            '-shared',
            '-Wl,-all_load,-force_load',
            goLib.toFilePath(),
            '-framework',
            'CoreFoundation',
            '-o',
            libUri.toFilePath(),
          ],
          logger: logger,
          captureOutput: false,
          throwOnUnexpectedExitCode: true,
        );
      }
    }

    if (assetName != null) {
      buildOutput.addAssets([
        NativeCodeAsset(
          package: buildConfig.packageName,
          name: assetName!,
          file: libUri,
          linkMode: linkMode,
          os: buildConfig.targetOS,
          architecture:
              buildConfig.dryRun ? null : buildConfig.targetArchitecture,
        )
      ]);
    }

    if (!buildConfig.dryRun) {
      final sources = [
        bridgePath,
        for (final source in this.sources)
          packageRoot.resolveUri(Uri.file(source)),
      ];
      final dartBuildFiles = [
        for (final source in this.dartBuildFiles) packageRoot.resolve(source),
      ];

      final sourceFiles = await Stream.fromIterable(sources)
          .asyncExpand(
            (path) => Directory(path.toFilePath())
                .list(recursive: true)
                .where((entry) => entry is File)
                .map((file) => file.uri),
          )
          .toList();

      buildOutput.addDependencies({
        ...sourceFiles,
        ...dartBuildFiles,
      });
    }
  }
}

LinkMode _linkMode(LinkModePreference preference) {
  if (preference == LinkModePreference.dynamic ||
      preference == LinkModePreference.preferDynamic) {
    return DynamicLoadingBundled();
  }
  assert(preference == LinkModePreference.static ||
      preference == LinkModePreference.preferStatic);
  return StaticLinking();
}

const androidNdkClangTargetFlags = {
  Architecture.arm: 'armv7a-linux-androideabi',
  Architecture.arm64: 'aarch64-linux-android',
  Architecture.ia32: 'i686-linux-android',
  Architecture.x64: 'x86_64-linux-android',
  Architecture.riscv64: 'riscv64-linux-android',
};
